import Random_Forest
import numpy as np
import sys

class Unsupervised_Random_Forest:

#Random Forest
    def _predict(self, forest, dataset):
        matrix_obserationsXtrees = forest.datapoints_leaf_trees(dataset)
        similarity_matrix = forest.generate_similarity_matrix(matrix_obserationsXtrees)
        dissimilarity_matrix = np.sqrt(np.subtract(1, similarity_matrix))
        return dissimilarity_matrix, matrix_obserationsXtrees, similarity_matrix

    def _predict_new_data_point (self, forest, matrix_obserationsXtrees, data_point):
        data_point_leaves_trees_results = forest.datapoints_leaf_trees(data_point)
        similarity_row, similarity_column = forest.calculate_data_point_similarity(data_point_leaves_trees_results, matrix_obserationsXtrees)
        dissimilarity_row = np.sqrt(np.subtract(1, similarity_row))
        dissimilarity_column=  np.sqrt(np.subtract(1, similarity_row))
        return dissimilarity_row, dissimilarity_column

    #Clustering
    def _offline_clustering(self, data_points_leaves_trees_results, dissimilarity_matrix, number_of_clusters):
        """Cluster the dataset using the choosen method

        :data_points_leaves_trees_results: observations to be clustered
        :dissimilarity_matrix: dissimilarity matrix, also generated by the method predict
        :number_of_clusters: Number of expected clusters
        :returns: groups and dissimilarity matrix (to be used to new dt points)

        """
        groups, medoids_index = self._pam(dissimilarity_matrix, number_of_clusters)
        return groups, medoids_index

#Clustering algorithms, using R
    def _pam(self, dissimilarity_matrix, number_of_clusters):
        from rpy2.robjects.packages import importr
        from rpy2.robjects.numpy2ri import numpy2ri
        import rpy2.robjects as ro
        from rpy2.robjects import r
        ro.conversion.py2ri = numpy2ri
        importr("cluster") 
        pam_results = r.pam(dissimilarity_matrix, k=number_of_clusters, diss=True)
        classes = np.subtract(np.array(pam_results[2]), 1) #R index start from 1, so se transform in a np array and subract 1
        medoids = np.subtract(np.array(pam_results[1]), 1) #R index start from 1, so se transform in a np array and subract 1
        return classes, medoids 
    def _create_synthetic_class(self, original_data):
        import random
        synthetic_data = np.empty(original_data.shape, original_data.dtype)
        for idx, dt_point in enumerate(original_data):
            for dimension in dt_point.dtype.names: #sample randomly the values of each dimension
                random_index = random.randint(0, original_data.shape[0]-1)
                synthetic_data [idx] [dimension] =  original_data [random_index][dimension]
        label_class_original_data = np.zeros(shape=(original_data.shape[0],1))
        label_class_synthetic_data = np.ones(shape=(original_data.shape[0],1))
        data_original_synthetic = np.concatenate((original_data, synthetic_data))
        label_data_points = np.concatenate((label_class_original_data.flatten(), label_class_synthetic_data.flatten())).flatten()
        return label_data_points, data_original_synthetic

    def _encode_one_of_n(self, Y):
        """
        Y: A size (n_data,) numpy vector of numeric class labels in the interval [0, n)

        Returns: A size (n_data, n) numpy matrix of class labels in a one-of-n
        encoding.
        """                                                           
        #creates an array with n dimensions where n is the number of classes and set 1 in the index of the class (if class 2 out of 3 = 0,1,0)
        Y = np.equal.outer(Y, np.arange(Y.max()+1)).astype(np.float) 
        assert np.all(Y.sum(axis=1) == 1.)
        return Y

    def train_forest(self, dataset, number_of_trees):
        """Build the number_of_trees trees in the, it uses bootstrap (67%) 
        :dataset: observations to build the forest
        :returns: the built forest
        """
        labels, data_orginal_synthetic = self._create_synthetic_class(dataset)
        forest = Random_Forest.ClusteringForest()
        forest.fit(
                data_orginal_synthetic,
                self._encode_one_of_n(labels),
                n_trees=number_of_trees,
                max_depth=600,
                n_min_leaf=1,
                n_trials=3
                )
        return forest


#PAM Interfaces
    #Batch module of Unsupervised RF - get all dataset, train the forest, and predict the same dataset
    def pam_offline_clustering(self, dataset, number_of_clusters, number_of_trees, forest = None):
        if not forest:
            print 'training offline pam'
            forest = self.train_forest(dataset, number_of_trees)
        dissimilarity_matrix, data_points_leaves_trees_results, similarity = self._predict(forest, dataset)
        groups, medoids_index = self._offline_clustering('PAM', data_points_leaves_trees_results, dissimilarity_matrix, number_of_clusters)
        return groups

    def pam_online_training(self, training_set, number_of_clusters, number_of_trees, forest = None):
        """Trains the forest with the dataset, i.e. doest no predict

        :training_set: @todo
        :prediction_set: @todo
        :number_of_clusters: @todo
        :returns: @todo

        """
        forest = self.train_forest(training_set, number_of_trees)
        dissimilarity_matrix, data_points_leaves_trees_results, similarity = self._predict(forest, training_set)
        groups, medoids_index = self._offline_clustering(data_points_leaves_trees_results, dissimilarity_matrix, number_of_clusters)
        return forest, data_points_leaves_trees_results[medoids_index]


    def pam_online_clustering(self, forest, medoids_leaves_trees_results, prediction_set):
        """Assign a class to each data point of the prediction set
        Requires the prediction set in a Numpy Array form, to do so you can use the read_file method

        :forest: forest returned by the pam_online_training method 
        :medoids_leaves_trees_results: the matrix with the leaves that the medoids finished in each tree of the forest
        :returns: the class of each data point

        """
        try:
            prediction_set.shape[0]
        except:
            prediction_set = np.reshape(prediction_set, 1)
        cluster_assigned = np.empty((prediction_set.shape[0]), np.int32)
        for id_data_point, data_point in enumerate(prediction_set):
            dissimilarity_row, dissimilarity_column = self._predict_new_data_point(forest, medoids_leaves_trees_results, data_point) #dissimilarity of the dt to the medoids_index
            #Gets the column of the smallest dissimilarity, that is the class (classses should be ordered)
            cluster_assigned [id_data_point] = dissimilarity_row.argmin()
        return cluster_assigned


    def pam_dissimilarity_between_dt_and_medoids(self, forest, medoids_leaves_trees_results, prediction_set):
        """
        :forest: forest returned by the pam_online_training method 
        :medoids_leaves_trees_results: the matrix with the leaves that the medoids finished in each tree of the forest
        :returns: a list with the 

        """
        try:
            prediction_set.shape[0]
        except:
            prediction_set = np.reshape(prediction_set, 1)
        data_points_dissimilarity_row = []
        for id_data_point, data_point in enumerate(prediction_set):
            dissimilarity_row, dissimilarity_column = self._predict_new_data_point(forest, medoids_leaves_trees_results, data_point) #dissimilarity of the dt to the medoids_index
            data_points_dissimilarity_row.append(dissimilarity_row[0])
        return data_points_dissimilarity_row


#dataset manipulations Interface
    def read_data_from_file(self, path, sample_size=0, random=False):
        #dataset = np.genfromtxt(path, None, delimiter=',', usecols=xrange(0,41))
        dataset = np.genfromtxt(path, None, delimiter=',')
        #with open(path) as data_file:
            #id_datapoints = {Tools.parse_line(line, idx)[0] : Tools.parse_line(line,idx)[1] for idx, line in enumerate(data_file) if line != '' }
        #dataset = Tools.fill_missing_values(id_datapoints)
        #ids  = np.asarray(id_datapoints.keys())
        if random:
            index_array = np.arange(dataset.shape[0]) #create an array with the number of the row
            np.random.shuffle(index_array)
            return dataset[index_array[:sample_size]]
        else:
            return dataset


    def rfpam_training_and_prediction(self, path_training_set, path_prediction_set, number_of_clusters = 8, number_of_trees = 100):
        """Trains the forest and outputs the cluster number of all observations in the precition set.
        :training_set: @todo
        :prediction_set: @todo
        :number_of_clusters: @todo
        :number_of_trees: @todo
        :returns: @todo
        """
        try:
            training_set = self.read_data_from_file(path_training_set, random=False)
        except Exception, e:
            raise 'Could not read the training set, please make sure it is in the correct format \n', e
        try:
            prediction_set = self.read_data_from_file(path_prediction_set, random=False)
        except Exception, e:
            raise 'Could not read the prediction set, please make sure it is in the correct format \n', e
        #train
        forest, medoids_leaves_trees_results = self.pam_online_training(training_set, number_of_clusters, number_of_trees)
        #predict 
        cluster_assignments = self.pam_online_clustering(forest, medoids_leaves_trees_results, prediction_set) 
        #save outputs
        return cluster_assignments


    def calculate_dissimilarity_with_forest (self, forest, prediction_set):
        dissimilarity_matrix, leaves_trees, similarity_matrix = self._predict(forest, prediction_set)
        return dissimilarity_matrix, similarity_matrix

    def calculate_dissimilarity(self, training_set, prediction_set, number_of_trees):
        #train
        forest = self.train_forest(training_set, number_of_trees)
        dissimilarity_matrix, leaves_trees, similarity_matrix = self._predict(forest, prediction_set)
        return dissimilarity_matrix, similarity_matrix

    def rfpam_interactive(self):
        path_training_set = raw_input('\nWhere is the training set (ex: ./dataset.cvs or data/test)? It is better to use less than 1000 observations to start...: \n\n')
        path_prediction_set = raw_input('\nWhere is the set to be clustered (ex: ./dataset.cvs or data/test)? It is better to use less than 1000 observations to start...: \n\n')
        number_of_trees = raw_input('\nHow many trees (default = 100)?\n')
        try:
            number_of_trees = int(number_of_trees)
        except Exception:
            print 'Could not read number of trees, using 100'
            number_of_trees = 100
        try:
            number_of_clusters = raw_input('\nHow many clusters (groups, default = 8)?\n')
            number_of_clusters = int(number_of_clusters)
        except Exception:
            print 'Could not read number of clusters, using 8'
            number_of_clusters = 8
        print '\nThe number is the cluster of each observation (in the order of the prediction set)\n\n',self.rfpam_training_and_prediction(path_training_set, path_prediction_set, number_of_clusters, number_of_trees)

    def rfpam_interactive_diss(self):
        """Trains the forest and outputs the cluster number of all observations in the precition set.
        :training_set: @todo
        :prediction_set: @todo
        :number_of_clusters: @todo
        :number_of_trees: @todo
        """
        #Read training  
        path_training_set = raw_input('\nWhere is the training set (ex: ./dataset.cvs or data/test)? It is better to use less than 1000 observations to start...: \n\n')
        number_of_trees = raw_input('\nHow many trees (default = 100)?\n')
        try:
            number_of_trees = int(number_of_trees)
        except Exception, e:
            print 'Could not read number of trees, using 100'
            number_of_trees = 100
        try:
            training_set = self.read_data_from_file(path_training_set, random=False)
        except Exception, e:
            raise RuntimeError('Could not read the training set, please make sure it is in the correct format', e)
        observation_one = raw_input('\nPlease, paste observation one and press enter:\n')
        observation_two = raw_input('\nPlease, paste observation two and press enter:\n')
        prediction_set = self.format_observations(observation_one, observation_two)
        observation_two = raw_input('\nCalculating dissimilarity...\n')
        dissimilarity, similarity = self.calculate_dissimilarity(training_set, prediction_set, number_of_trees)
        print '\nThe similarity between these two observations is: ', similarity[0,1]
        print '\nThe dissimilarity (sqr of 1-similarity) between this two observations is: ', dissimilarity[0,1], '\n'

    def format_observations(self, observation_one, observation_two):
        """Format 2 observations to calculate the dissimilarity

        :observation_one: @todo
        :observation_two: @todo
        :returns: @todo

        """
        from StringIO import StringIO
        observations = StringIO(observation_one + '\n' + observation_two)
        prediction_set= np.genfromtxt(observations, None, delimiter=',')
        return prediction_set 
#Main
if __name__ == '__main__':
    rfpam = Unsupervised_Random_Forest()
    interactive = raw_input('\nPress 1 to cluster services and 2 to check the dissimilarity between two observations (and then press enter)\n')
    try:
        interactive = int(interactive)
    except Exception:
        print 
    if interactive is 1:
        rfpam.rfpam_interactive()
    elif interactive is 2:
        rfpam.rfpam_interactive_diss()
    else:
        print 'Number not recognised, going to the clustering'
        rfpam.rfpam_interactive()
